#include <cmath>

#include "audio.h"
#include "core.h"
#include "font.h"
#include "game.h"
#include "image.h"
#include "keyboard.h"
#include "screen.h"
#include "time.h"
#include <OpenGL/gl.h>

ScreenManager *screen_manager;

struct {
    Screen *title;
    Screen *game;
} screens;

struct {
    font::Font *impact_large;
    font::Font *impact;
    font::Font *impact_small;
    font::Font *fixed;
} fonts;

struct {
    audio::SoundId zap;
    audio::SoundId bang1;
    audio::SoundId bang2;
} sounds;

struct {
    image::Image *ship;
} images;

inline double wrap(double x, double w) {
    return fmod(fmod(x, w) + w, w);
}

const double ship_deadtime = 2;

int rand_int(int n);
int rand_int(int n) {
    return int(n * (random() * 1.0 / RAND_MAX));
}

// Draw a starfield. t will normally be gametime::GlobalTime().
// NB: calls srandom(), breaking future randomness.
void DrawStarField(double t);
void DrawStarField(double t) {
    srandom(42);
    glEnable(GL_BLEND);
    glBegin(GL_QUADS);
    for (int i = 0; i < 400; ++i) {
        double speed = 3.0;
        double angle = random() * CONST_TWOPI / RAND_MAX;
        double ph = random() * 1.0 / RAND_MAX;
        double radius = 20 + 600 * fmod(t + ph * speed, speed) / speed;
        radius = radius * radius / 600;
        radius = radius * radius / 600;
        double sz = random() * 5.0 / RAND_MAX + 1.0;
        sz = sz * radius / 400.0;
        if (sz < 1) sz = 1;
        u32 base_col = RGB(255, 255, rand_int(50) + 210);
        u32 col = COL_MUL(base_col, RGBA(255, 255, 255, 80 + radius));
        SetColor(col);
        double x = SCREEN_WIDTH / 2 + cos(angle) * radius - sz/2;
        double y = SCREEN_HEIGHT / 2 + sin(angle) * radius - sz/2;
        if (radius < 20) continue;
        glVertex2d(x, y);
        glVertex2d(x + sz, y);
        glVertex2d(x + sz, y + sz);
        glVertex2d(x, y + sz);
    }
    glEnd();
    glDisable(GL_BLEND);
    SetColor(COL_WHITE);
}


class GameScreen : public Screen {
public:
    void ship_reset() {
        ship.x = SCREEN_WIDTH / 2;
        ship.y = SCREEN_HEIGHT / 2;
        ship.r = 0;
        ship.vx = 0;
        ship.vy = 0;
        ship.safe = 3;
        ship.dead = 0;
    }

    void new_wave() {
        for (int i = 0; i < 3; ++i) {
            double x, y;
            do {
                x = SCREEN_WIDTH * (random() * 1.0 / RAND_MAX);
                y = SCREEN_HEIGHT * (random() * 1.0 / RAND_MAX);
            } while (pow(x - SCREEN_WIDTH/2, 2) + pow(y - SCREEN_HEIGHT / 2, 2) < 300 * 300);
            create_asteroid(x, y, int(6 * (random() * 1.0 / RAND_MAX)));
        }        
    }
    
    void Init() {
        bullet_count = 0;
        asteroid_count = 0;
        ship_reset();
        new_wave();
        score = 0;
        lives = 3;
    }
    void create_asteroid(double x, double y, int size) {
        if (asteroid_count == N_ELEMS(asteroid)) return;
        int i = asteroid_count++;
        asteroid[i].x = x;
        asteroid[i].y = y;
        asteroid[i].r = random() * CONST_TWOPI / RAND_MAX;
        asteroid[i].size = size;
        double theta = random() * CONST_TWOPI / RAND_MAX;
        double speed = (random() * 1.0 / RAND_MAX) * 50 + 20;
        if (size == 0) speed += 50;
        if (size == 1) speed += 20;
        asteroid[i].vx = cos(theta) * speed;
        asteroid[i].vy = sin(theta) * speed;        
    }

    void create_bullet(double x, double y, double r, double vx, double vy, double t) {
        if (bullet_count == N_ELEMS(bullet)) return;
        int i = bullet_count++;
        bullet[i].x = x - sin(r) * 20;
        bullet[i].y = y + cos(r) * 20;
        bullet[i].vx = -400 * sin(r) + vx;
        bullet[i].vy = 400 * cos(r) + vy;
        bullet[i].t = t;
    }
    void create_explosion(double x, double y, double max_t, double min_r, double max_r, u32 color) {
        if (explosion_count == N_ELEMS(explosion)) return;
        int i = explosion_count++;
        explosion[i].x = x;
        explosion[i].y = y;
        explosion[i].min_r = min_r;
        explosion[i].max_r = max_r;
        explosion[i].t = 0;
        explosion[i].max_t = max_t;
        explosion[i].color = color;
    }
    virtual void Tick(double dt, bool active) {
        if (!active) return;
        if (asteroid_count == 0) {
            new_wave();
            ship.safe = 3;
        }
        if (ship.dead > 0) {
            ship.dead -= dt;
            if (ship.dead <= 0) {
                lives--;
                if (lives) {
                    ship_reset();
                } else {
                    screen_manager->TransitionToScreen(screens.title, TRANSITION_FADE_OUT);
                    return;
                }
            }
        } else {
            if (keyboard::IsKeyDown(keyboard::KEY_O)) {
                ship.r += dt * CONST_TWOPI;
            }
            if (keyboard::IsKeyDown(keyboard::KEY_P)) {
                ship.r -= dt * CONST_TWOPI;
            }
            ship.r = fmod(ship.r, CONST_TWOPI);
            if (ship.dead == 0) {
                ship.safe -= dt;
                if (ship.safe < 0) ship.safe = 0;
            }
            if (keyboard::IsKeyDown(keyboard::KEY_Q)) {
                ship.vx -= dt * 1000 * sin(ship.r);
                ship.vy += dt * 1000 * cos(ship.r);
            }
            if (keyboard::IsKeyJustDown(keyboard::KEY_A)) {
  	        audio::PlaySound(sounds.zap, 0.2f);
                create_bullet(ship.x, ship.y, ship.r, ship.vx, ship.vy, 0.8);
            }
            ship.vx = ship.vx * pow(0.3, dt);
            ship.vy = ship.vy * pow(0.3, dt);
            ship.x += ship.vx * dt;
            ship.y += ship.vy * dt;
            ship.x = wrap(ship.x, SCREEN_WIDTH);
            ship.y = wrap(ship.y, SCREEN_HEIGHT);
            if (ship.safe == 0 && asteroid_intersection(ship.x, ship.y, 30) != -1) {
                ship.dead = ship_deadtime;
                create_explosion(ship.x, ship.y, ship_deadtime, 20, 90, RGB(255, 0, 0));
                audio::PlaySound(sounds.bang2);
            }
        }
        for (int i = 0; i < explosion_count; ++i) {
            explosion[i].t += dt;
            if (explosion[i].t > explosion[i].max_t) {
                explosion[i--] = explosion[--explosion_count];
                continue;
            }
        }
        for (int i = 0; i < bullet_count; ++i) {
            bullet[i].t -= dt;
            if (bullet[i].t <= 0) {
                bullet[i--] = bullet[--bullet_count];
                continue;
            }
            bullet[i].x = wrap(bullet[i].x + bullet[i].vx * dt, SCREEN_WIDTH);
            bullet[i].y = wrap(bullet[i].y + bullet[i].vy * dt, SCREEN_HEIGHT);
            int asteroid = asteroid_intersection(bullet[i].x, bullet[i].y, 0);
            if (asteroid != -1) {
                bullet[i--] = bullet[--bullet_count];
                destroy_asteroid(asteroid);
                continue;
            }
        }
        for (int i = 0; i < asteroid_count; ++i) {
            asteroid[i].x += dt * asteroid[i].vx;
            asteroid[i].y += dt * asteroid[i].vy;
            asteroid[i].x = wrap(asteroid[i].x, SCREEN_WIDTH);
            asteroid[i].y = wrap(asteroid[i].y, SCREEN_HEIGHT);
        }
    }
    int asteroid_intersection(double x, double y, double r) {
        for (int i = 0; i < asteroid_count; ++i) {
            double ax = asteroid[i].x;
            double ay = asteroid[i].y;
            double ar = asteroid[i].radius();
            if ((x - ax) * (x - ax) + (y - ay) * (y - ay) <= (ar + r) * (ar + r)) {
                return i;
            }
        }
        return -1;
    }

    void destroy_asteroid(int i) {
        double x = asteroid[i].x;
        double y = asteroid[i].y;
        int size = asteroid[i].size;
        score += 1000 + 1010 * size * size;
        asteroid[i] = asteroid[--asteroid_count];
        int n = size == 0 ? 0 : 2 + size;
        int sz = 0;
        for (int k = 0; k < n; ++k) {
            create_asteroid(x, y, sz);
            if (k % 3 == 2) sz += 1;
            if (sz == size) sz = size - 1;
        }
        create_explosion(x, y, 1, 5 + 10 * size, 50 + 20 * size, RGBA(128, 128, 128, 200));
        audio::PlaySound(sounds.bang1, 0.5f);
    }
    virtual void Draw() {
        DrawStarField(gametime::GlobalTime() * 0.3);
        {
            char score_text[32];
            sprintf(score_text, "%010d", score);
            font::Draw(fonts.impact, 560, SCREEN_HEIGHT - 40)
                .fixed_advance(24)
                .add_color(RGB(255, 255, 255))
                .with_outline(RGB(50, 20, 00))
                .draw(score_text);
        }
        glEnable(GL_BLEND);
        for (int i = 0; i < lives - 1; ++i) {
            images.ship->draw(SCREEN_WIDTH - 20 - i * 45, 30)
                .draw();
        }
        if (ship.dead == 0) {
            if (ship.safe > 0) {
                SetColor(RGBA(255, 255, 255, 200));
            }
            images.ship->draw(ship.x, ship.y)
                .rotated(ship.r)
                .draw();
            SetColor(COL_WHITE);
            glDisable(GL_BLEND);
            if (ship.safe > 0) {
                int c = ship.safe < 1 ? 200 + 100 * gametime::Wave(0.2) : 255;
                double r = 30 + 10 * (1 - ship.safe) / 0.2;
                r = CLAMP(r, 30, 40);
                SetColor(RGB(c, c, c));
                glLineWidth(1.0);
                glBegin(GL_LINE_STRIP);
                for (int i = 0; i < 20; ++i) {
                    double theta = CONST_TWOPI * (i % 19) / 19.0;
                    glVertex2d(ship.x + r * cos(theta),
                               ship.y + r * sin(theta));
                }
                glEnd();
                SetColor(COL_WHITE);
            }
        }
        glPointSize(5);
        SetColor(COL_WHITE);
        glBegin(GL_POINTS);
        for (int i = 0; i < bullet_count; ++i) {
            glVertex2d(bullet[i].x, bullet[i].y);
        }
        glEnd();
        for (int i = 0; i < asteroid_count; ++i) {
            for (int dx = -1; dx <= 1; ++dx) {
                for (int dy = -1; dy <= 1; ++dy) {
                    draw_asteroid(asteroid[i].x + SCREEN_WIDTH * dx,
                                  asteroid[i].y + SCREEN_HEIGHT * dy,
                                  asteroid[i].r, asteroid[i].size);                    
                }
            }
        }
        for (int i = 0; i < explosion_count; ++i) {
            draw_explosion(explosion[i].x, explosion[i].y, explosion[i].min_r, explosion[i].max_r,
                           explosion[i].t / explosion[i].max_t, explosion[i].color);
        }
    }
    void draw_explosion(double x, double y, double min_r, double max_r, double t, u32 color) {
        double it = 1 - (1 - t) * (1 - t);
        double inner = min_r + max_r * it * it;
        double outer = min_r * 1.5 + max_r * 0.7 * it;
        SetColor(COL_MUL(color, RGBA(255, 255, 255, 255 - 255 * t)));
        glEnable(GL_BLEND);
        glPushMatrix();
        glTranslated(x, y, 0);
        glBegin(GL_TRIANGLE_STRIP);
        for (int i = 0; i < 20; ++i) {
            double theta = CONST_TWOPI * (i % 19) / 19.0;
            glVertex2d(inner * cos(theta), inner * sin(theta));
            glVertex2d(outer * cos(theta), outer * sin(theta));
        }
        glEnd();
        glPopMatrix();
        glDisable(GL_BLEND);

    }
    void draw_asteroid(double x, double y, double r, int size) {
        glPushMatrix();
        glTranslated(x, y, 0);
        glRotated(r * 180 / CONST_PI, 0, 0, 1);
        double s = (1 + size) * 10;
        SetColor(COL_WHITE);
        glDisable(GL_BLEND);
        glLineWidth(1);
        glBegin(GL_LINE_STRIP);
        int N = 5 + 2 * size;
        for (int i = 0; i < N + 1; ++i) {
            double ds = 1 + cos((i % N) * 1.8) * 0.1;
            double theta = CONST_TWOPI * (i % N) * 1.0 / N;
            glVertex2d(ds * s * cos(theta), ds * s * sin(theta));
        }
        glEnd();
        glPopMatrix();
    }
private:
    int score;
    int lives;
    struct {
        double x, y;
        double r;
        double vx, vy;
        double safe;
        double dead;
    } ship;
    struct {
        double x, y;
        double vx, vy;
        double t;
    } bullet[100];
    int bullet_count;
    struct {
        double x, y;
        double vx, vy;
        double r;
        int size;
        double radius() {return (1 + size) * 10.0;}
    } asteroid[500];
    int asteroid_count;
    struct {
        double x, y;
        double min_r, max_r;
        u32 color;
        double t, max_t;
    } explosion[100];
    int explosion_count;
};

// The start screen.
class TitleScreen : public Screen {
    virtual void Tick(double dt, bool active) {
        if (!active) return;
        if (keyboard::IsKeyJustDown(keyboard::KEY_SPACE) ||
                keyboard::IsKeyJustDown(keyboard::KEY_Q) ||
                keyboard::IsKeyJustDown(keyboard::KEY_A)) {
            screens.game->Init();
            screen_manager->TransitionToScreen(screens.game, TRANSITION_FADE_OUT);
        }
    }
    
    virtual void Draw() {
        font::Draw(fonts.impact_large, 400, SCREEN_HEIGHT - fonts.impact_large->Height() - 140)
            .centered()
            .add_color(RGB(255, 50, 100))
            .add_color(RGB(50, 255, 100))
            .add_color(RGB(100, 50, 255))
            .add_color(RGB(220, 220, 80))
            .with_jiggle(3, 4.0)
            .with_outline(RGB(255, 255, 250))
            .draw("Jamsteroids");
        font::Draw(fonts.impact_small, 400, 200)
            .centered()
            .add_color(RGB(200, 200, 220))
            .draw("Q=forward  A=fire  O=left  P=right");
        double theta = gametime::Wave2(2.0);
        int t = 180 + 70 * theta;
        u32 pulsed_white = RGB(t, t, t);
        font::Draw(fonts.impact, 400, 100)
            .centered()
            .add_color(pulsed_white)
            .with_outline(RGB(0, 0, 0))
            .draw("PRESS START");        
    }
};



class MyGame : public game::Game {
public:
    MyGame() : game::Game() {}
    virtual ~MyGame() {
    }
    virtual void Init() {
        fonts.impact_large = font::Font::Load("BorisBlackBloxx.ttf", 90);
        fonts.impact = font::Font::Load("BorisBlackBloxx.ttf", 28);
        fonts.impact_small = font::Font::Load("BorisBlackBloxx.ttf", 20);
        fonts.fixed = font::Font::Load("VeraMono.ttf", 32);
        sounds.zap = audio::LoadSound("zap.wav");
        sounds.bang1 = audio::LoadSound("bang1.wav");
        sounds.bang2 = audio::LoadSound("bang2.wav");
        screens.title = new TitleScreen();
        screens.game = new GameScreen();
        screen_manager = new ScreenManager(screens.title);
        images.ship = image::Image::Load("spaceship.png");
    }
    virtual void Tick(double dt) {
        screen_manager->Tick(dt);
    }
    virtual void Draw() {
        glClearColor(0, 0, 0.1, 1);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        screen_manager->Draw();
    }
};

namespace game {

    game::Game* CreateGame() {
        return new MyGame();
    }

}  // namespace game
